未分类
299 词

欢来到我的博客,在我认真学习的大学三年里,我发现无论是用平板手写笔记,还是onenote笔记或者是单纯的云笔记,都无法满足我的记录需要,比如笔记间的跳转引用什么的(就像写程序一样!),后来我发现Markdown虽然可以支持跳转,但是既然我都要填地址了,干啥不填网址呢?而且还可以随时随地访问查看,温故而知新~

于是我决定用Hexo来搭建一个博客,并且用Github来托管,这样我就可以随时随地访问我的笔记/博客了,而且还可以用Markdown来写博客并且到处跳转,简直完美!

另外也算圆了一个小时候的目标吧~拥有一个看起来酷酷的网站。✅

希望你也能从这里找到需要的知识,时间就是生命,开源万岁!

38k 词

来点前后端

ok,最近要搞前端项目练手,来点spring boot

1.0.后端:Spring Boot?这是什么?

找到springboot的教程,开始部署开发环境。

不过我觉得直接从IDEA开局貌似更好?

直接在github上找到Spring的开源项目[点我去看看]
简单瞅一眼,发现有这个框架在IDEA中部署开发的教程,让我看看

Steps:
Within your locally cloned spring-framework working directory:

Precompile spring-oxm with ./gradlew :spring-oxm:compileTestJava
Import into IntelliJ (File -> New -> Project from Existing Sources -> Navigate to directory -> Select build.gradle)
When prompted exclude the spring-aspects module (or after the import via File-> Project Structure -> Modules)

OK,gradle的部署方式,那在IDEA中从已有的远程项目中创建。

欸?好像启动不了一个可运行的示例,我再找找…原来是可以使用Spring Initializr直接创建一个项目啊,看来之前安装的Spring-framework项目如果要启动要使用集成测试,属于是新手误入Boss房了。

好,Spring Initializr生成出来的项目小很多,使用IDEA启动并完成构建之后,我们就有了一个简易的Spring Boot项目了。

1.1.将SpringBoot连接到我的Mysql

Step1.数据库准备

Mysql部分:使用Navicat轻松创建表格定义数据类型,这里我创建了一个test数据库后在库中定义一个user表方便我们后续的类定义以及其他的功能。

**user表格(用户)**:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE user (
userid INT AUTO_INCREMENT PRIMARY KEY,-- 用户ID,自增主键
username VARCHAR(50) NOT NULL UNIQUE,-- 用户名,唯一且不能为空
password VARCHAR(255) NOT NULL,-- 密码,不能为空
gender ENUM('male', 'female', 'other') NOT NULL, -- 性别,枚举类型:male、female、other
birthdate DATE,-- 出生日期,格式为YYYY-MM-DD
address VARCHAR(255),-- 家庭住址,字符串类型
peopleid VARCHAR(20) NOT NULL,-- 身份证号,字符串类型,不能为空
peoplename VARCHAR(20) NOT NULL,-- 真实姓名,字符串类型,不能为空
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 用户创建时间,默认为当前时间
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 用户信息更新时间,默认为当前时间,更新时自动更新
);

**medical_records表格(病历)**:
这个表一个外键,使其与users表中的userid相关联

1
2
3
4
5
6
7
8
9
10
CREATE TABLE medical_records (
record_id INT AUTO_INCREMENT PRIMARY KEY, -- 主键
userid INT NOT NULL, -- 关联到用户表的userid,注意这里使用INT类型
diagnosis_date DATE NOT NULL, -- 诊断日期
doctor VARCHAR(255) NOT NULL, -- 医生姓名
diagnosis TEXT NOT NULL, -- 诊断结果
remark TEXT, -- 备注信息
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userid) REFERENCES user(userid) ON DELETE CASCADE
);

在IDEA中,点击窗口右侧的数据库图标,在里面添加我们的数据库,笔者这里用的Mysql,其他厂商的数据库你在下拉列表里找到就行了。
Alt text
点击后填写我们的数据库信息,数据库方面不是本文重点,这里不过多笔墨了。

Step2.配置项目依赖并重新构建

SpringBoot部分:我使用的是gradle构建,因此我在build.gradle中配置必要的依赖项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.3'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'


// MySQL Connector/J
runtimeOnly 'mysql:mysql-connector-java:8.0.33' // 根据具体项目调整版本号

// Spring Data JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Spring Web Starter 依赖
implementation 'org.springframework.boot:spring-boot-starter-web'

//Junit,后面测试连通性用到的依赖
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
// 确保包含AssertJ
testImplementation 'org.assertj:assertj-core:3.24.2'

}

tasks.named('test') {
useJUnitPlatform()
}

如果飘红,就按照IDEA的引导引入对应的依赖库就行了。

mysql-connector的版本和Mysql版本有关,我的Mysql版本是8.0,所以这里使用8.0的Connector,具体的Connector版本和Mysql版本的对照可以到Mysql官网查看

添加完成依赖之后,运行build.gradle完成构建

Step3.配置数据库连接

application.properties中添加数据库连接配置,当然,如果我们的spring版本比较旧,可能是application.yml之类的文件,这里我贴出我的application.propertiesyml格式的文件不支持直接粘贴进去。

1
2
3
4
5
6
7
spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTC
## 自己改your_database_name
spring.datasource.username=your_username
## 自己改your_username
spring.datasource.password=your_password
## 自己改your_password
spring.jpa.hibernate.ddl-auto=update

Step4.创建用户实体类

根据我们刚刚新建的User表创建JPA实体类
Alt text

然后,进入User类,IDEA生成的JPA代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package com.example.demo.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;

import java.time.Instant;
import java.time.LocalDate;

@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "userid", nullable = false)
private Integer id;

@Column(name = "username", nullable = false, length = 50)
private String username;

@Column(name = "password", nullable = false)
private String password;

@Lob
@Column(name = "gender", nullable = false)
private String gender;

@Column(name = "birthdate")
private LocalDate birthdate;

@Column(name = "address")
private String address;

@Column(name = "pepoleid", nullable = false, length = 20)
private String pepoleid;

@Column(name = "pepolename", nullable = false, length = 20)
private String pepolename;

@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "created_at")
private Instant createdAt;

@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "updated_at")
private Instant updatedAt;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public LocalDate getBirthdate() {
return birthdate;
}

public void setBirthdate(LocalDate birthdate) {
this.birthdate = birthdate;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

public String getPepoleid() {
return pepoleid;
}

public void setPepoleid(String pepoleid) {
this.pepoleid = (String) pepoleid;
}

public String getPepolename() {
return pepolename;
}

public void setPepolename(String pepolename) {
this.pepolename = pepolename;
}

public Instant getCreatedAt() {
return createdAt;
}

public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}

public Instant getUpdatedAt() {
return updatedAt;
}

public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}

}

然后你会看到@Table(name = "user")这一行中有警告,根据IDEA的提示给他重新分配数据源到我们刚刚创建的user数据库中
Alt text

完成之后你Ctrl+鼠标左键点击这个"user"应该是能触发你IDEA弹出数据库的弹窗的,这样就对了。

另外,你会发现在User类在com.example.demo中,为了方便管理,我们在com.example.demo下创建一个entity包,并将User类移动到这个包中。

做完这些之后的项目结构:
Alt text

对病历表实体进行同样的操作,将MedicalRecord类移动到com.example.demo.entity包中。
MedicalRecord

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package com.example.demo.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import java.time.Instant;
import java.time.LocalDate;

@Entity
@Table(name = "medical_records")
public class MedicalRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "record_id", nullable = false)
private Integer id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "userid", nullable = false)
private User userid;

@Column(name = "diagnosis_date", nullable = false)
private LocalDate diagnosisDate;

@Column(name = "doctor", nullable = false)
private String doctor;

@Lob
@Column(name = "diagnosis", nullable = false)
private String diagnosis;

@Lob
@Column(name = "remark")
private String remark;

@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "created_at")
private Instant createdAt;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public User getUserid() {
return userid;
}

public void setUserid(User userid) {
this.userid = userid;
}

public LocalDate getDiagnosisDate() {
return diagnosisDate;
}

public void setDiagnosisDate(LocalDate diagnosisDate) {
this.diagnosisDate = diagnosisDate;
}

public String getDoctor() {
return doctor;
}

public void setDoctor(String doctor) {
this.doctor = doctor;
}

public String getDiagnosis() {
return diagnosis;
}

public void setDiagnosis(String diagnosis) {
this.diagnosis = diagnosis;
}

public String getRemark() {
return remark;
}

public void setRemark(String remark) {
this.remark = remark;
}

public Instant getCreatedAt() {
return createdAt;
}

public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}

}

Step5.编写数据访问层Repository

在项目中src/main/java/com.example.demo/下创建一个repository包,并在其中创建UserRepositoryMedicalRecordRepository接口,代码如下:

UserRepository:

1
2
3
4
5
6
7
8
9
10
package com.example.demo.repository;

import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
User findByUsername(String username);
}

MedicalRecordRepository:

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.demo.repository;

import com.example.demo.entity.MedicalRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface MedicalRecordRepository extends JpaRepository<MedicalRecord, Integer> {
List<MedicalRecord> findByUserid(Integer userid);
}

注意UserRepositoryMedicalRecordRepositoryInterface而不是Class

Step7.编写测试方法,检测连通性

转到src/test/java/com.example.demo/,注意是test中,创建一个repository包,添加UserRepositoryTest类用来测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package com.example.demo.repository;

import com.example.demo.entity.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.annotation.Rollback;

import java.time.LocalDate;

import static org.assertj.core.api.Assertions.assertThat;



@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
//↑这句非常重要,JPA单元测试时,会自动切换到内嵌数据库,使用这个语句把它关掉,不然你头想破了都不知道为什么测试连不上数据库
public class UserRepositoryTest {

@Autowired
private UserRepository userRepository;

@Autowired
private TestEntityManager entityManager;

private User testUser;
@BeforeEach
void setUp() {
System.out.println("Setting up test environment...");
// 创建测试用户
testUser = new User();
testUser.setUsername("zzb");
testUser.setPassword("password");
testUser.setGender("male");
testUser.setBirthdate(LocalDate.of(1998, 3, 3));
testUser.setAddress("Example Address");
testUser.setPepolename("zzb");
testUser.setPepoleid("123456789123456789");
// 将测试用户插入到数据库中
entityManager.persist(testUser);
System.out.println("Inserted user ID: " + testUser.getId());
}

@Test
@Rollback(false)//←控制测试创建的数据是否回滚,这里用false,防止出现"数据库连接幻觉"情况
public void whenFindByUsername_thenReturnUser() {
System.out.println("Running whenFindByUsername_thenReturnUser test...");
User foundUser = userRepository.findByUsername(testUser.getUsername());

assertThat(foundUser).isNotNull().usingRecursiveComparison().isEqualTo(testUser);

if (foundUser == null) {
System.out.println("User not found by username.");
} else {
System.out.println("Found user with ID: " + foundUser.getId());
}
}

@Test
public void testSaveUser() {
// 创建新用户并保存
User newUser = new User();
newUser.setUsername("zyy");
newUser.setPassword("newpassword");
newUser.setGender("female"); // 使用String类型的gender
newUser.setBirthdate(LocalDate.of(1993, 3, 3));
newUser.setAddress("New Example Address");
newUser.setPepolename("zyy");
newUser.setPepoleid("12345678912345678X");

User savedUser = userRepository.save(newUser);

// 确认新用户的ID已生成(即成功保存)
assertThat(savedUser.getId()).isNotNull();

// 可选:验证新用户是否可以被找到
User foundUser = userRepository.findByUsername("zyy");
assertThat(foundUser).isNotNull().usingRecursiveComparison().isEqualTo(savedUser);

if (savedUser == null || foundUser == null) {
System.out.println("Failed to save or find the new user.");
} else {
System.out.println("Saved and found user with ID: " + savedUser.getId());
}
}
}

做完这些之后我们的项目结构
Alt text
你可能会发现AutoConfigureTestDatabaselocalDate这两个类没有导入,导入一下就完事了。
Alt text

搞定,重新构建一下,没有报错就可以跑一下测试了

1
./gradlew test

测试完成,没有报错,刷新表之后在Navicat中看到我们测试用例创建的新用户
Alt text
搞定~

Step8:给前端提供功能

对于用户相关的操作,我们提供注册、登录等功能。
其中,能操作所有数据的管理员功能我们写到controller
而普通用户需要用到的操作,我们写到service

首先我们创建一个service包,然后创建UserServiceMedicalRecordService接口

UserService:

1
2
3
4
5
6
7
8
9
10
package com.example.demo.service;

import com.example.demo.entity.User;

public interface UserService {
User validateUser(String username, String password);
User registerUser(User user) throws IllegalArgumentException; // 注册用户
User updateUserInfo(Integer userId, User userDetails) throws IllegalArgumentException; // 修改用户信息
void updatePassword(Integer userId, String oldPassword, String newPassword) throws IllegalArgumentException; // 修改密码
}

MedicalRecordService:

1
2
3
4
5
6
7
8
9
10
package com.example.demo.service;

import com.example.demo.entity.MedicalRecord;

import java.util.List;

public interface MedicalRecordService {
List<MedicalRecord> getMedicalRecordsByUserId(Integer userId);
}

然后,我们用再在service内创建实现其功能的包impl,在service.impl内创建实现这个接口的类UserServiceImplMedicalRecordServiceImpl

UserServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.example.demo.service.impl;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserRepository userRepository;

@Override
public User validateUser(String username, String password) {
User user = userRepository.findByUsername(username);
if (user != null && user.getPassword().equals(password)) {
return user;
}
return null;
}

@Override
public User registerUser(User user) throws IllegalArgumentException {
// 检查用户名是否已存在
if (userRepository.findByUsername(user.getUsername()) != null) {
throw new IllegalArgumentException("用户名已存在");
}
// 检查密码长度
if (user.getPassword().length() < 6 || user.getPassword().length() > 255) {
throw new IllegalArgumentException("密码长度必须在 6 到 255 个字符之间");
}
// 保存用户
return userRepository.save(user);
}

@Override
public User updateUserInfo(Integer userId, User userDetails) throws IllegalArgumentException {
Optional<User> optionalUser = userRepository.findById(userId);
if (optionalUser.isPresent()) {
User user = optionalUser.get();
user.setUsername(userDetails.getUsername());
user.setGender(userDetails.getGender());
user.setBirthdate(userDetails.getBirthdate());
user.setAddress(userDetails.getAddress());
user.setPepoleid(userDetails.getPepoleid());
user.setPepolename(userDetails.getPepolename());
return userRepository.save(user);
} else {
throw new IllegalArgumentException("用户不存在");
}
}

@Override
public void updatePassword(Integer userId, String oldPassword, String newPassword) throws IllegalArgumentException {
Optional<User> optionalUser = userRepository.findById(userId);
if (optionalUser.isPresent()) {
User user = optionalUser.get();
// 验证旧密码
if (!user.getPassword().equals(oldPassword)) {
throw new IllegalArgumentException("旧密码错误");
}
// 更新密码
user.setPassword(newPassword);
userRepository.save(user);
} else {
throw new IllegalArgumentException("用户不存在");
}
}
}

MedicalRecordServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.demo.service.impl;

import com.example.demo.entity.MedicalRecord;
import com.example.demo.repository.MedicalRecordRepository;
import com.example.demo.service.MedicalRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MedicalRecordServiceImpl implements MedicalRecordService {

@Autowired
private MedicalRecordRepository medicalRecordRepository;

@Override
public List<MedicalRecord> getMedicalRecordsByUserId(Integer userId) {
return medicalRecordRepository.findByUserid(userId);
}
}

对于查询所有账户的信息,对账户信息的增删改查等管理员权限的功能,我们写在controller包的UserController类中,当然,为了调用方便,我们也在这个类中应用UserService的方法。

UserController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/users")
public class UserController {

//调用UserService接口,登录的验证使用UserService
@Autowired
private UserService userService;

// 登录接口
@PostMapping("/login")

public ResponseEntity<?> login(@RequestBody User loginRequest) {
// 调用 UserService 验证用户
User user = userService.validateUser(loginRequest.getUsername(), loginRequest.getPassword());
if (user != null) {
// 登录成功,返回用户信息
return ResponseEntity.ok(user);
} else {
// 登录失败,返回错误信息
return ResponseEntity.badRequest().body("用户名或密码错误");
}
}

//调用UserRepository接口,对账户的控制函数都实现于本文件内。
@Autowired
private UserRepository userRepository;

// 获取所有用户
@GetMapping
public List<User> getAllUsers() {
return userRepository.findAll();
}

// 根据ID获取用户
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable(value = "id") Integer userId) {
Optional<User> user = userRepository.findById(userId);
if (user.isPresent()) {
return ResponseEntity.ok().body(user.get());
} else {
return ResponseEntity.notFound().build();
}
}

// 创建新用户
@PostMapping
public User createUser(@RequestBody User newUser) {
return userRepository.save(newUser);
}

// 更新用户信息
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable(value = "id") Integer userId, @RequestBody User userDetails) {
Optional<User> optionalUser = userRepository.findById(userId);
if (optionalUser.isPresent()) {
User user = optionalUser.get();
user.setUsername(userDetails.getUsername());
user.setPassword(userDetails.getPassword());
user.setGender(userDetails.getGender());
user.setBirthdate(userDetails.getBirthdate());
user.setAddress(userDetails.getAddress());
user.setPepoleid(userDetails.getPepoleid());
user.setPepolename(userDetails.getPepolename());
final User updatedUser = userRepository.save(user);
return ResponseEntity.ok(updatedUser);
} else {
return ResponseEntity.notFound().build();
}
}

// 删除用户
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable(value = "id") Integer userId) {
Optional<User> user = userRepository.findById(userId);
if (user.isPresent()) {
userRepository.delete(user.get());
return ResponseEntity.ok().build();
} else {
return ResponseEntity.notFound().build();
}
}
}

注意添加完成后重新构建一下。

2.0.前端:VUE?这是什么?

好吧,找一下发现VUE是JavaScript的框架,那还是要Node.js的运行环境,装就完事了。node.js的安装还是很简单的,笔者是Windows11环境,使用pnpm安装就完事了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Download and install fnm:
winget install Schniz.fnm

# Download and install Node.js:
fnm install 22

# Verify the Node.js version:
node -v # Should print "v22.14.0".

# Download and install pnpm:
corepack enable pnpm

# Verify pnpm version:
pnpm -v

ok,安装完成,node -v能出现版本号了。

Vite:强大的Vue构建工具

OK,导师说装个Vite更方便,问一下AI这玩意干啥的。

Generated from Qwen:
Vite 是一个现代化的前端构建工具,旨在显著提升前端开发体验。它主要由两大部分组成:一个是开发服务器,另一个是用于生产环境的构建指令。

装就完事了

1
npm install -g create-vite

OK装好之后就可以用create-vite来方便地创建一个Demo了。

创建并进入一个我们打算存放Demo的地方,然后运行

1
2
3
4
5
6
7
8
9
10
create-vite vue_demo --template vue
Scaffolding project in

vue_demo...

Done. Now run:

cd vue_demo
npm install
npm run dev

OK,接下来按照指引运行一下这三个命令,就能运行我们的Demo了。


安装和配置必要的组件库

当然,此时的Demo还是空的,我们还需要安装一些必要的组件库,比如axios、vue-router、Element Plus等,才能满足我们对页面的各种各样的需求。

axios

安装 Axios:

Axios用于发送Http请求给后端API。

1
npm install axios
配置 Axios:

我们还需要创建一个文件来封装它,以便于管理和维护。

  1. 创建一个新的文件 src/api/index.js
1
2
3
4
5
6
7
8
9
import axios from 'axios';

const instance = axios.create({
baseURL: 'http://localhost:8080/api/', // 设置默认的API地址
timeout: 5000, // 请求超时时间
headers: {'Content-Type': 'application/json'}
});

export default instance;
  1. 创建 axios 实例

src目录下创建一个utils文件夹,并在其中创建一个http.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

import axios from 'axios';

const http = axios.create({
baseURL: 'https://api.example.com', // 我们的 API 地址
timeout: 5000, // 请求超时时间
});

// 请求拦截器
http.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
return config;
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error);
}
);

// 响应拦截器
http.interceptors.response.use(
(response) => {
// 对响应数据做些什么
return response.data;
},
(error) => {
// 对响应错误做些什么
return Promise.reject(error);
}
);

export default http;
  1. 在需要调用 API 的地方引入并使用这个实例:
1
import http from '@/utils/http';

vue-router

安装 Vue Router:

Vue Router用于管理页面路由,也就是我们在网页中经常用到的页面跳转功能。

1
npm install vue-router
配置 Vue Router:

接下来,我们为 Vue 应用程序设置路由管理。

  1. 创建路由配置文件 src/router/index.js

这里病历页面使用/medical-records/:userId的方式进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/Register.vue'),
},
{
path: '/medical-records/:userId',
name: 'MedicalRecords',
component: () => import('@/views/MedicalRecords.vue'),
}
];

const router = createRouter({
history: createWebHistory(),
routes,
});

export default router;
  1. main.jsmain.ts 中引入并使用路由:
1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

const app = createApp(App);
app.use(router);//router
app.use(ElementPlus);//el
app.mount('#app');

Element Plus

安装 Element Plus:

最后安装Element Plus用于快速开发UI界面。
顺便再装一个自动引入的插件,这样我们就可以直接使用Element Plus的组件而无需手动导入每个组件。

1
2
npm install element-plus
npm install unplugin-auto-import unplugin-vue-components -D
配置 Element Plus:

对于 Element Plus,我们可以选择完整引入或按需引入。这里推荐使用按需引入以减少打包体积。

修改 vite.config.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path'; // 需要引入 path 模块

export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'), // 将 @ 指向 src 目录
},
},
});

这样,你就可以在我们的组件中直接使用 Element Plus 的组件而无需手动导入每个组件。

通过以上步骤,你应该能够在我们的 Vite + Vue 项目中成功配置和使用 axiosvue-routerElement Plus。记得根据实际需求调整配置和代码逻辑。


页面编写

好了,接下来就可以开始写登录界面的代码了。
我们在之前配置 Vue Router的时候已经指定了HomeLoginRegister三个文件的路径,他们是在views目录下的,所以我们要先在根目录src下创建views文件夹,然后在里面创建Home.vueLogin.vueRegister.vue,分别对应首页、登录页和注册页,MedicalRecords.vue则是病历的动态页面。

之后,我们的项目目录应该看起来像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

src/
├── assets/
├── components/
├── router/
│ └── index.js
├── utils/
│ └── http.js
├── views/
│ ├── Home.vue
│ ├── Login.vue
│ ├── MedicalRecords.vue
│ └── Register.vue
├── App.vue
└── main.js


Home.vue:

随便写点东西在Home(其实一开始是想在这个界面显示数据库中最近创建的16个用户,不过写完是一坨bug,所以就删掉了,不完整但是无伤大雅)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
<el-container>
<el-main>
<h1>Welcome to Home Page</h1>
<el-button type="primary" @click="fetchData">Fetch Data</el-button>

<!-- 数据展示 -->
<el-table :data="tableData" v-loading="loading" style="margin-top: 20px">
<el-table-column prop="id" label="ID" width="100" />
<el-table-column prop="title" label="Title" />
</el-table>
</el-main>
</el-container>
</template>

<script>
import http from '@/utils/http';

export default {
name: 'Home',
data() {
return {
tableData: [], // 表格数据
loading: false, // 加载状态
};
},
methods: {
async fetchData() {
this.loading = true;
try {
// 调用 API 获取数据
const response = await http.get('/posts'); // 假设 API 返回一个帖子列表
this.tableData = response;
} catch (error) {
this.$message.error('Failed to fetch data');
console.error(error);
} finally {
this.loading = false;
}
},
},
};
</script>

<style scoped>
h1 {
color: #409EFF;
}

.el-button {
margin-bottom: 20px;
}
</style>

Login.vue:

登录成功之后使用router将UserId传给之后的页面,用于传递用户信息,当然使用会话也很好,有验证机制,这里先用URL保存用户ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<template>
<el-container class="login-container">
<el-main>
<el-card class="login-card">
<h2>用户登录</h2>
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" label-width="80px">
<!-- 用户名 -->
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username" placeholder="请输入用户名" />
</el-form-item>

<!-- 密码 -->
<el-form-item label="密码" prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
/>
</el-form-item>

<!-- 登录按钮 -->
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</el-container>
</template>

<script>
import http from '@/utils/http'; // 引入 axios 实例
import { ElMessage } from 'element-plus'; // 引入 Element Plus 的消息提示组件

export default {
name: 'Login',
data() {
return {
loginForm: {
username: '',
password: '',
},
loginRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 255, message: '密码长度在 6 到 255 个字符', trigger: 'blur' },
],
},
};
},
methods: {
async handleLogin() {
try {
await this.$refs.loginFormRef.validate();
const response = await http.post('/login', this.loginForm);
if (response.code === 200) {
ElMessage.success('登录成功');
const userId = response.data.userId;
localStorage.setItem('user', JSON.stringify(response.data));
this.$router.push({ name: 'MedicalRecords', params: { userId } });
} else {
ElMessage.error(response.message || '登录失败');
}
} catch (error) {
ElMessage.error('登录失败,请检查用户名和密码');
console.error(error);
}
},
resetForm() {
this.$refs.loginFormRef.resetFields();
},
},
};
</script>

<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f5f5f5;
}

.login-card {
width: 400px;
padding: 20px;
}

h2 {
text-align: center;
margin-bottom: 20px;
color: #409EFF;
}
</style>

效果是这样
Alt text


Register.vue:

给每个输入都做了判定和单独的提示,如果输入不合法,则不会发送请求,并且会有提示。

其中,比较有创新的地方是

  1. 按照出生日期在前端实时刷新年龄,当前时间减去出生日期,并将计算结果显示在前端。
  2. 限制了出生日期只能选择在今天以前的日期。
  3. 对身份证号有特殊检查,只能是18位,并且前17位必须是数字,最后一位可以是数字或者X。

其他数据的检查稀松平常,看代码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
<template>
<el-container class="register-container">
<el-main>
<el-card class="register-card">
<h2>用户注册</h2>
<el-form :model="registerForm" :rules="registerRules" ref="registerFormRef" label-width="100px">
<!-- 用户名 -->
<el-form-item label="用户名" prop="username">
<el-input v-model="registerForm.username" placeholder="请输入用户名" />
</el-form-item>

<!-- 密码 -->
<el-form-item label="密码" prop="password">
<el-input v-model="registerForm.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>

<!-- 确认密码 -->
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="请再次输入密码" show-password />
</el-form-item>

<!-- 性别 -->
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="registerForm.gender">
<el-radio label="male"></el-radio>
<el-radio label="female"></el-radio>
<el-radio label="other">其他</el-radio>
</el-radio-group>
</el-form-item>

<!-- 出生日期和年龄 -->
<el-row>
<el-col :span="12">
<el-form-item label="出生日期" prop="birthdate">
<el-date-picker
v-model="registerForm.birthdate"
type="date"
placeholder="选择日期"
@change="calculateAge"
:disabled-date="disabledDate"/>
</el-form-item>


</el-col>
<el-col :span="12">
<el-form-item label="年龄">
<el-input :value="age" disabled></el-input>
</el-form-item>
</el-col>
</el-row>

<!-- 地址 -->
<el-form-item label="地址" prop="address">
<el-input v-model="registerForm.address" placeholder="请输入地址" />
</el-form-item>

<!-- 姓名 -->
<el-form-item label="姓名" prop="realName">
<el-input v-model="registerForm.realName" placeholder="请输入您的真实姓名" />
</el-form-item>

<!-- 身份证号 -->
<el-form-item label="身份证号" prop="idNumber">
<el-input v-model="registerForm.idNumber" placeholder="请输入您的身份证号码" />
</el-form-item>

<!-- 注册按钮 -->
<el-form-item>
<el-button type="primary" @click="handleRegister">注册</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</el-container>
</template>

<script>
import http from '@/utils/http'; // 引入 axios 实例
import { ElMessage } from 'element-plus'; // 引入 Element Plus 的消息提示组件

export default {
name: 'Register',
data() {
const validateConfirmPassword = (rule, value, callback) => {
if (value !== this.registerForm.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
};

return {
registerForm: {
username: '',
password: '',
confirmPassword: '',
gender: 'male',
birthdate: null,
address: ''
},
age: null, // 新增用于存储计算出来的年龄
registerRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 255, message: '密码长度在 6 到 255 个字符', trigger: 'blur' },
],
confirmPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' },
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' },
],
birthdate: [
{ required: true, message: '请选择出生日期', trigger: 'change' },
],
address: [
{ required: true, message: '请输入地址', trigger: 'blur' },
],
realName: [
{ required: true, message: '请输入您的真实姓名', trigger: 'blur' },
],
idNumber: [
{ required: true, message: '请输入您的身份证号码', trigger: 'blur' },
{ min: 18, max: 18, message: '身份证号码必须是18位', trigger: 'blur' }, // 检查长度
{
pattern: /^[0-9]{17}[0-9X]$/, // 正则表达式,确保只包含数字和最后一位可能是X
message: '身份证号码格式不正确,只能包含数字和最后一位可能是大写字母X',
trigger: 'blur'
}
],
},
};
},
methods: {
disabledDate(time) {
return time.getTime() > Date.now(); // 只能选择今天之前(不含今天)的日期
},
calculateAge() {
if (this.registerForm.birthdate) {
const today = new Date();
const birthDate = new Date(this.registerForm.birthdate);
let years = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();

if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
years--;
}

this.age = years;
} else {
this.age = null;
}
},
async handleRegister() {
try {
await this.$refs.registerFormRef.validate();
const response = await http.post('/api/users/register', this.registerForm);
if (response.code === 200) {
ElMessage.success('注册成功');
this.$router.push('/login'); // 跳转到登录页面
} else {
ElMessage.error(response.message || '注册失败');
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '注册失败,请检查输入');
console.error(error);
}
},
resetForm() {
this.$refs.registerFormRef.resetFields();
this.age = null; // 重置时也需重置年龄
},
},
};
</script>

<style scoped>
.register-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f5f5f5;
}

.register-card {
width: 500px;
padding: 20px;
}

h2 {
text-align: center;
margin-bottom: 20px;
color: #409EFF;
}
</style>

效果截图贴这,我主要技术栈是单片机方向的,所以页面主打一个能用就行,不太美观。

Alt text

在酒吧点个炒饭看看
Alt text
好,全防住了

MedicalRecords.vue

通过URL传递的用户ID,对后端进行访问,查询到用户的病历数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<template>
<div class="medical-records">
<!-- 病历列表 -->
<el-table :data="medicalRecords" style="width: 100%">
<el-table-column prop="diagnosisDate" label="诊断日期" width="180"></el-table-column>
<el-table-column prop="doctor" label="医生" width="180"></el-table-column>
<el-table-column prop="diagnosis" label="诊断"></el-table-column>
<el-table-column prop="remark" label="备注"></el-table-column>

<el-table-column label="操作">
<template slot-scope="scope">
<el-button @click="handleDetail(scope.row)" type="text" size="small">查看详情</el-button>
</template>
</el-table-column>
</el-table>

<!-- 病历详情对话框 -->
<el-dialog title="病历详情" :visible.sync="dialogVisible">
<p>诊断日期: {{ currentRecord.diagnosisDate }}</p>
<p>医生: {{ currentRecord.doctor }}</p>
<p>诊断: {{ currentRecord.diagnosis }}</p>
<p>备注: {{ currentRecord.remark }}</p>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
import { useRoute } from 'vue-router';

export default {
data() {
return {
userId: null,
medicalRecords: [], // 动态获取的病历数据
dialogVisible: false,
currentRecord: {}
};
},
created() {
const route = useRoute();
this.userId = route.params.userId; // 从路由参数中获取userId
this.fetchMedicalRecords(); // 页面加载时调用此方法获取病历数据
},
methods: {
fetchMedicalRecords() {
},
handleDetail(record) {
this.currentRecord = record;
this.dialogVisible = true;
}
}
};
</script>

<style scoped>
.medical-records {
padding: 20px;
}
</style>

效果图
Alt text

系统漏洞分析

  1. 后端没有编写对发送过来的表单进行二次审查的功能,所以很容易被攻击输入非法数据甚至是SQL注入,安全性几乎是0。
  2. 用户输入的所有数据都是透明传输,所以用户输入的数据很容易被窃取。

虽然这些系统上的问题都可以通过增加功能进行防御,我都想写,但是时间上来不及了,手上也没有现成的开源模块可以直接塞进去(手上能用的是Python的代码),不过还是在这里简单总结一下,谨留以后效。

结语

至此,我们完成了对用户注册和登录功能的开发,并添加了表单验证和错误处理。现在,用户可以注册他们的账户,并且系统会根据输入的数据进行验证,确保数据的完整性和准确性现在我们了解了怎么在IDEA中用SpringBoot进行数据库方面的开发,另外学了一些前端的知识,也算是前端成功入门了,技能+++++……

本文使用过的AI工具:

Qwen
DeepSeek

在遇到一些技术问题,比如在测试中怎么取消测试用例的回滚,以及怎么防止测试用例访问自己的H2数据库而不是我的Mysql这些问题上,感谢这些AI帮我解决了这些关键问题,顺便还了解了JPA和EntityManager的知识,爽!AI技术进步和开源真好,在AI工具的帮助下,我学东西都快了不少。

735 词

shell控制语句

if语句

注意 if 后面要接 then,else后不需要,用fi表示结束if语句。

1
2
3
4
5
6
7
if [ condition ]; 
then # do something
elif [ condition ];
then # do something
else
# do something
fi

case语句

case后面接变量+in,相当于C语言中的switch

value + )表示case的值,可以用|将多个运行值连接

"*"是在这里被视作通配符,可以用类似"Bob*")过滤出Bob开头的变量的情况,可以用单一个"*"表示C语言中default的情况,

;;表示跳出case语句段,

esac表示结束case语句

1
2
3
4
5
6
7
8
9
10
11
echo "This script will print your choice"
case $1 in
"one" | "1" | "first")
echo "You have chosen one"
;;
"two" | "2" | "second")
echo "You have chosen two"
;;
*)
echo "Error Please try Again"
esac

for循环

1
2
3
4
for var in item1 item2 ... itemN
do
# do something
done

while循环

1
2
3
4
while condition
do
# do something
done

until循环

1
2
3
4
until condition
do
# do something
done
593 词

主机字节序

主机字节序就是主机存储数据的顺序,有大端序小端序两种

  • 大端序:高位字节存储在低地址上,低位字节存储在高地址上
  • 小端序:低位字节存储在低地址上,高位字节存储在高地址上

网络字节序

网络字节序就是网络传输数据的顺序,规定为大端序

主机字节序转网络字节序

htonl

Host to Network Long
将主机字节序转换成网络字节序

1
2
3
4
#include <arpa/inet.h>

unsigned long int htonl(
unsigned long int hostlong);

htons

Host to Network Short
将主机字节序转换成网络字节序

1
2
3
4
#include <arpa/inet.h>

unsigned short int htons(
unsigned short int hostshort);

网络字节序转主机字节序

ntohl

Network to Hos Long
将网络字节序转换成主机字节序

1
2
3
4
#include <arpa/inet.h>

unsigned long int ntohl(
unsigned long int netlong);

ntohs

Network to Host Short
将网络字节序转换成主机字节序

1
2
3
4
#include <arpa/inet.h>

unsigned short int ntohs(
unsigned short int netshort);
2.2k 词

测试语句

在编写shell脚本时,常遇到需要判断变量的值师傅相等,或者检查文件状态等,这时就需要测试语句进行测试才能决定下一步动作。shell脚本根据不同的数据类型设置了不同的条件语句的格式,使用test或者用中括号包围就可以执行条件测试语句,下面介绍常用的测试语句。


数值测试

二元测试语句
数值测试用于比较两个数值的大小,常用的测试语句如下:

  • -eq:等于(equal)
  • -ne:不等于(not equal)
  • -gt:大于(greater than)
  • -lt:小于(less than)
  • -ge:大于等于(greater than or equal to)
  • -le:小于等于(less than or equal to)

示例:

1
2
3
4
5
6
7
8
#!/bin/bash
num1=10
num2=20
if [ $num1 -eq $num2 ]; then
echo "$num1 等于 $num2"
else
echo "$num1 不等于 $num2"
fi

字符串测试

二元测试语句
字符串测试用于比较两个字符串是否相等,常用的测试语句如下:

  • =:等于
  • !=:不等于
  • -z:字符串长度为0
  • -n:字符串长度不为0

示例:

1
2
3
4
5
6
7
8
#!/bin/bash
str1="hello"
str2="world"
if [ $str1 = $str2 ]; then
echo "$str1 等于 $str2"
else
echo "$str1 不等于 $str2"
fi

文件测试

文件测试分为文件状态测试和文件比较测试,以下是状态测试的部分

文件状态测试

一元测试语句
文件测试用于检查文件的状态,常用的测试语句如下:

  • -e:文件存在(exist)
  • -s:文件是否非空
  • -S:文件是套接字文件(socket)
  • -L:文件是软链接文件(symbolic link)
  • -b:文件是块设备文件(block)
  • -c:文件是字符设备文件(character)
  • -d:文件是目录(dir)
  • -f:文件是普通文件(file)
  • -p:文件是管道文件(pipe)
  • -r:文件可读(readable)
  • -w:文件可写(writeable)
  • -x:文件可执行(executable)

示例:

1
2
3
4
5
6
7
#!/bin/bash
file="/etc/passwd"
if [ -e $file ]; then
echo "$file 存在"
else
echo "$file 不存在"
fi

文件比较测试

二元测试语句
文件比较测试用于比较文件的大小,修改时间等信息,常用的测试语句如下:

  • -nt:文件1比文件2新(newer than)
  • -ot:文件1比文件2旧(older than)
  • -ef:文件1和文件2是同一个文件(equals file)(判断两个文件是否是硬链接)

另外,还可以借用数值测试中的比较运算符进行文件大小比较,常用的测试语句如下:

  • -eq:文件1和文件2大小相等(equal)
  • -ne:文件1和文件2大小不相等(not equal)
  • -le:文件1小于等于文件2(less than or equal)
  • -ge:文件1大于等于文件2(greater than or equal)

示例:

1
2
3
4
5
6
7
8
#!/bin/bash
file1="/etc/passwd"
file2="/etc/shadow"
if [ $file1 -nt $file2 ]; then
echo "$file1$file2 新"
else
echo "$file1$file2 旧"
fi

复合测试

复合测试用于将多个测试条件组合在一起,常用的测试语句如下:

命令执行控制语句:

  • &&:逻辑与(and)
  • ||:逻辑或(or)

命令执行控制语句本质上是将多个命令的执行结果进行运算,并不能直接的将多个判断条件放在一条命令中

示例:

1
test -e /home && test -r /home
1
2
3
4
5
6
7
#!/bin/bash
file="/etc/passwd"
if [ -e $file ] && [ -r $file ]; then
echo "$file 存在并且可读"
else
echo "$file 不存在或者不可读"
fi

多重条件判定语句:

  • -a:逻辑与(and)
  • -o:逻辑或(or)
  • !:逻辑非(not)

和命令执行控制语句不同,多重条件判断语句就是将多个条件用-a或者-o连接起来,如果条件为真,则整个表达式为真,否则为假。

示例:

1
test -e /home -a -r /home

可以看到这条示例中相比于前面的命令执行控制语句,少了一个 test ,这是因为在多重条件判断语句中,-a-o 是默认的,所以可以省略,同时被视作一条命令。

1
2
3
4
5
6
7
#!/bin/bash
file="/etc/passwd"
if [ -e $file -a -r $file ]; then
echo "$file 存在并且可读"
else
echo "$file 不存在或者不可读"
fi

条件表达式

条件表达式用于在shell脚本中执行条件判断,常用的测试语句如下:

1.7k 词

概念

Shell 是一个命令行解释器,它为用户提供了一个向操作系统发送指令的接口。Shell 脚本(Shell Script)是一种为 Shell 编写的脚本程序,它包含了一系列的命令和语句,用于实现特定的功能。Shell 脚本通常用于自动化日常任务、批量处理文件、编写简单的应用程序等。
Shell脚本本质上是Shell命令的有序集合。


基本语法

1
2
3
4
5
6
7
#!/bin/bash

# 注释
# 开头指定bash解释脚本,不写则默认shell

# 屏幕打印字符串:
echo "hello world"

变量

自定义变量

1
2
3
4
5
6
7
8
# 定义变量
name="hello"
# 使用变量
echo $name
# 只读变量
readonly name
# 删除变量
unset name

环境变量

shell在开始运行时就已经定义了一些和系统工作环境有关的变量,我们在shell中可以直接使用$name 引用。
可以使用 export 前缀来自定义环境变量,或将已有变量修饰为环境变量。
另外,使用env命令可以查看当前系统所有的环境变量。环境变量使用unset也可以清除。
一般情况下环境变量是全大写字母

1
2
3
4
# 定义环境变量
export name="hello"
# 使用环境变量
echo $name

常见的环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
# 当前用户
USER
# 当前用户家目录
HOME
# 当前用户登录的终端
SHELL
# 当前用户登录的终端类型
TERM
# 当前用户登录的终端的编号
Tty
# 当前用户登录的终端的进程号
Pid
# 当前用户登录的终端的进程号

访问和编辑系统变量文件

一般在 ~/.bashrc或者/etc/profile文件中,可以编辑环境变量。
编辑完成后,使用 source ~/.bashrc 或者 source /etc/profile 命令使修改生效。或者重启终端/系统让其自动启用生效。

预设变量

在运行shell脚本时,我们可以通过命令行参数的形式向脚本传递参数,这些参数在脚本中被称为预设变量。预设变量是shell脚本中预先定义好的变量,用于存储命令行参数的值。这样在shell脚本中就可以调用用户在执行脚本时传入的参数。

$0:当前进程的名称

$$:当前进程的进程号

$1 $2 $3 …… $9:运行脚本时传递给其的参数。如果序号不止一位,必须使用花括号将序号连成整体${10}

$#:参数个数

$*:所有参数的内容(拼接字符串)

$@:所有参数组成的数组(变量数组)

$?:命令返回后的状态,0表示成功,非0表示失败

字符串

1
2
3
4
5
6
7
8
9
# 字符串拼接
name="hello"
echo "my name is ${name}"
# 字符串长度
name="hello"
echo ${#name}
# 字符串截取
name="hello"
echo ${name:1:2}

数组

1
2
3
4
5
6
# 定义数组
arr=(1 2 3)
# 使用数组
echo ${arr[0]}
# 数组长度
echo ${#arr[@]}

条件判断

1
2
3
4
5
6
7
8
# if
if [ $1 -eq 1 ]; then
echo "1"
elif [ $1 -eq 2 ]; then
echo "2"
else
echo "3"
fi

循环

1
2
3
4
# for
for i in {1..5}; do
echo $i
done

函数

1
2
3
4
5
6
7
8

# 定义函数
function func() {
echo "hello"
}
# 调用函数
func

读取文件

1
2
3
4
# 读取文件
while read line; do
echo $line
done < file.txt

执行脚本的三种方式

  1. chmod +x test.sh,后 ./test.sh
    增加可执行权限后执行
  2. bash test.sh
    直接指定bash解释运行脚本
  3. . test.sh
    .空格 + 脚本 使用当前shell执行脚本,这种执行方式不会开启子进程,脚本中的变量会直接在当前shell中生效。而且会忽略脚本中指定的shell解释器类型。
1k 词

符号用法

"":包含的变量会被解释,同时会识别解释其内的特殊字符。

'':包含的内容当作纯字符串原样输出,不会解释其内的特殊字符。例如:

1
2
3
4
5
name = "zhangsan"
string1 = "good morning $name"
string2 = 'good morning $name'
echo $string1
echo $string2

输出结果:

1
2
good morning zhangsan
good morning $name

``$():反引号(括号)中的内容作为系统命令,执行其内容,可以替换输出为一个变量,例如

1
$echo "todys is `date`"

输出结果:

1
todys is Mon Jul 29 14:15:00 CST 2024

\:转义符,除了可以像C语言中一样使用\n \t \r等转义字符为符号,将原来有特殊含义的字符变为普通字符,如\$等,如果要让echo指令识别转义符,则需要在echo命令前面加上-e提示符。

():括号,在括号中的命令会新开一个子shell顺序执行,括号外的命令在当前shell中执行,括号中如果出现变量,则变量作用域仅限于括号中,括号外无法使用括号内的变量。

{}:大括号,大括号中的命令在当前shell顺序执行,大括号中可以使用当前shell的变量。
注意{}内的命令因为是在当前的shell中执行,所以在回括前的最后一条语句也需要分号
例如

1
2
3
4
5
6
7
8
num = 100
# 在子shell中执行,不影响当前的变量
( num = 999; echo "1.1: $num" )
echo "1.2: $num";
# 在当前shell执行,共用括号外部的变量
{ num = 666; echo "2.1: $num"; } # 注意大括号在回括前的最后一条语句需要分号
echo "2.2: $num"

输出结果:

1
2
3
4
1.1: 999
1.2: 100
2.1: 666
2.2: 666

[]:条件判断,在[]中的条件判断,条件前后必须有空格,例如:[ $var -eq 1 ]


`${}`:变量取值替换,在变量名前加上`$`符号,或者使用`${}`符号,可以获取变量的值。例如:`echo ${var}` 或 `echo $var`。
2.5k 词

文件指针

文件指针就是用于标识一个文件的,程序中所有对文件的操作都需要通过文件指针执行。

文件指针的一般形式

FILE* 指针变量标识符;

FILE为大写,需要引用<stdio.h>

FILE是系统使用typedef定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息

一般情况下,我们操作文件前必须定义一个文件指针标示我们将要操作的文件

实际编程中使用库函数操作文件,无需关心FILE结构体的细节,只需要将文件指针传给io库函数,库函数再通过FILE结构体里的信息对文件进行操作。

FILE 结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
short level; //缓冲区“满”或“空”的程度
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如无缓冲区不读取字符
short bsize; //缓冲区的大小
unsigned char *buffer; //数据缓冲区的位置
unsigned ar; //文件位置指示器
unsigned char *curp; //文件位置指示器
unsigned istemp; //临时文件指示器
short token; //用于有效性检查
} FILE;

对文件操作的步骤:

  1. 对文件进行读写等操作之前要打开文件得到文件指针
  2. 可以通过文件指针对文件进行读写等操作
  3. 读写等操作完毕后,要关闭文件,关闭文件后,就不能再通过此文件指针操作文件了

文件指针位置操作函数

ftell

获取文件指针的位置

1
long ftell(FILE *stream);

ftell函数返回stream标识的文件指针的位置。

返回值:

  • 成功:返回文件指针的位置
  • 失败:返回-1

参数:

  • stream:要获取的文件指针

fseek

设置文件指针的位置

1
2
3
4
int fseek(
FILE *stream,
long offset,
int whence);

fseek函数将stream标识的文件指针移动到whence指定的位置,移动的偏移量为offset

返回值:

  • 成功:返回0

  • 失败:返回非0

参数:

  • stream:要设置的文件指针
  • offset:要移动的偏移量
    offset的取值
    • 大于0:向文件末尾移动
    • 小于0:向文件开头移动
    • 等于0:不移动
  • whence:要移动的位置
    whence的取值:
    • SEEK_SET(0):从文件开头开始移动
    • SEEK_CUR(1):从文件当前位置开始移动
    • SEEK_END(2):从文件末尾开始移动

移动文件指针的结束条件:

  • 移动到文件的末尾
  • 移动到文件的size-1个字符
  • 移动到文件结束符EOF

rewind

将文件指针重置到文件开头

1
void rewind(FILE *stream);

rewind函数将stream标识的文件指针重置到文件开头。

返回值:

参数:

  • stream:要重置的文件指针

文件操作函数

fopen

打开文件并得到文件指针

1
2
3
FILE *fopen(
const char *filename,
const char *mode);

文件指针的初始化:

1
2
3
4
5
6
7
FILE *fp = NULL;
fp = fopen("test.txt", "r");
if (fp == NULL)
{
printf("文件打开失败\n");
return 0;
}

返回值:

  • 成功:返回文件指针
  • 失败:返回NULL

参数:

  • filename:要打开的文件路径
  • mode:打开文件的方式

文件打开路径(filename):

  • 绝对路径:从盘符开始的路径C:/test/abc.txt
  • 相对路径:从当前路径开始的路径./abc.txt

文件打开方式(mode):

模式 说明
r 只读,文件必须存在
w 只写,文件不存在则创建,存在则清空
a 只写,文件不存在则创建,存在则追加
r+ 读写,文件必须存在
w+ 读写,文件不存在则创建,存在则清空
a+ 读写,文件不存在则创建,存在则追加

文件打开模式(mode)的补充:

+的意义:

  • 可以同时进行读写操作
  • 文件指针在打开文件时默认指向文件开头,r+w+会清空文件;a+不会清空文件,文件指针在打开文件时默认指向文件末尾。

fclose

关闭文件

1
int fclose(FILE *fp);

返回值:

  • 成功:返回0
  • 失败:返回EOF

参数:

  • fp:要关闭的文件指针

关闭文件的意义:

  • 关闭文件后,文件指针将不再指向该文件,不能通过该文件指针对文件进行操作
  • 关闭文件后,文件缓冲区中的数据会写入文件,文件指针指向文件末尾
  • 关闭文件后,文件缓冲区会被释放,文件指针指向NULL

关闭文件的方式:

  • 使用fclose函数关闭文件
  • 程序结束时,操作系统会自动关闭所有打开的文件
  • 程序结束时,如果文件没有被关闭,操作系统会自动关闭文件,但会输出警告信息

关闭文件的意义:

  • 关闭文件后,文件指针将不再指向该文件,不能通过该文件指针对文件进行操作
  • 关闭文件后,文件缓冲区中的数据会写入文件,文件指针指向文件末尾
  • 关闭文件后,文件缓冲区会被释放,文件指针指向NULL

feof

判断文件指针是否到达文件末尾

1
int feof(FILE *stream);

feof函数判断stream标识的文件指针是否到达文件末尾。

返回值:

  • 到达文件末尾:返回非0
  • 未到达文件末尾:返回0

参数:

  • stream:要判断的文件指针

ferror

判断文件操作是否出错

1
int ferror(FILE *stream);

ferror函数判断stream标识的文件操作是否出错。

返回值:

  • 出错:返回非0

  • 未出错:返回0

参数:

  • stream:要判断的文件指针

clearerr

清除文件指针的错误标志

1
void clearerr(FILE *stream);

clearerr函数清除stream标识的文件指针的错误标志。

返回值:

参数:

  • stream:要清除的文件指针

fflush

刷新文件缓冲区

1
int fflush(FILE *stream);

fflush函数刷新stream标识的文件缓冲区。

返回值:

  • 成功:返回0

  • 失败:返回EOF

参数:

  • stream:要刷新的文件指针
4.4k 词

文件读写字符函数

fgetc

从文件中读取一个字符

1
int fgetc(FILE *fp);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

int main()
{
FILE *fp = NULL;
fp = fopen("test.txt", "r");
if (fp == NULL)
{
printf("文件打开失败\n");
return 0;
}
int ch = fgetc(fp);
while (ch != EOF)
{
printf("%c", ch);//打印字符
ch = fgetc(fp);
}
fclose(fp);
return 0;

}

返回值:

  • 成功:返回读取到的字符
  • 失败:返回EOF

参数:

  • fp:要读取的文件指针

读取字符的方式:

  • 从文件指针的位置开始读取
  • 每次读取一个字符
  • 读取到的字符会自动转换为ASCII码

读取字符的结束条件:

  • 读取到文件末尾
  • 读取到文件结束符EOF

fgets

从文件中读取一行字符

1
char *fgets(char *str, int n, FILE *fp);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>

int main()
{
FILE *fp = NULL;
fp = fopen("test.txt", "r");
if (fp == NULL)
{
printf("文件打开失败\n");
return 0;
}
char str[100];
while (fgets(str, 100, fp) != NULL)
{
printf("%s", str);
}
fclose(fp);
return 0;

}

返回值:

  • 成功:返回读取到的字符串
  • 失败:返回NULL

参数:

  • str:要读取的字符串
  • n:读取的字符数
  • fp:要读取的文件指针

读取字符串的方式:

  • 从文件指针的位置开始读取
  • 每次读取一行字符
  • 读取到的字符会自动转换为ASCII码
  • 读取完成后在输出的字符串末尾添加\0

读取字符串的结束条件:

  • 读取到文件的换行符
  • 读取到文件的size-1个字符
  • 读取到文件结束符EOF

fputc

向文件中写入一个字符

1
int fputc(int ch, FILE *fp);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

int main()
{
FILE *fp = NULL;
fp = fopen("test.txt", "w");
if (fp == NULL)
{
printf("文件打开失败\n");
return 0;
}
int ch = 'a';
while (ch <= 'z')
{
fputc(ch, fp);
ch++;
}
fclose(fp);
return 0;

}

返回值:

  • 成功:返回写入的字符
  • 失败:返回EOF

参数:

  • ch:要写入的字符
  • fp:要写入的文件指针

写入字符的方式:

  • 从文件指针的位置开始写入
  • 每次写入一个字符
  • 写入的字符会自动转换为ASCII码

fputs

向文件中写入字符串

1
int fputs(const char *str, FILE *fp);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>

int main()
{
FILE *fp = NULL;
fp = fopen("test.txt", "w");//以写入的方式打开文件
if (fp == NULL)
{
printf("文件打开失败\n");
return 0;
}
char str[] = "hello world";
fputs(str, fp);
fclose(fp);
return 0;

}

返回值:

  • 成功:返回写入的字符串
  • 失败:返回EOF

参数:

  • str:要写入的字符串
  • fp:要写入的文件指针

写入字符串的方式:

  • 从文件指针的位置开始写入
  • 写入的字符会自动转换为ASCII码

写入字符串的结束条件:

  • 读取到字符串的末尾
  • 读取到字符串的换行符
  • 读取到字符串的\0

文件读写函数

fread

从文件中读取数据

1
2
3
4
5
size_t fread(
void *ptr,
size_t size,
size_t nmemb,
FILE *stream);

fread函数从stream标识的文件中读取数据,一元素的大小由size决定,共nmemb个元素,存放在ptr指定的内存中。

返回值:

  • 成功:返回实际读取到的元素个数
  • 失败:返回0

参数:

  • ptr:要读取的数据的地址
  • size:每个元素的大小
  • nmemb:要读取的元素个数
  • stream:要读取的文件指针

读取数据的方式:

  • 从文件指针的位置开始读取
  • 每次读取一个元素
  • 读取到的数据会自动转换为ASCII码

fwrite

向文件中写入数据

1
2
3
4
5
size_t fwrite(
const void *ptr,
size_t size,
size_t nmemb,
FILE *stream);

fwrite函数向stream标识的文件中写入数据,一元素的大小由size决定,共nmemb个元素,从ptr指定的内存中读取。

返回值:

  • 成功:返回实际写入的元素个数
  • 失败:返回0

参数:

  • ptr:要写入的数据的地址
  • size:每个元素的大小
  • nmemb:要写入的元素个数
  • stream:要写入的文件指针

写入数据的方式:

  • 从文件指针的位置开始写入
  • 每次写入一个元素
  • 写入的数据会自动转换为ASCII码

注意

  • fread/fwrite函数读写数据时,会自动跳过文件中的空格、换行符等字符
  • fread/fwrite函数可以将文件的内容解析为结构体等其他格式进行读写,而非字符类型的数据会以二进制格式保存在文件中
    例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>

typedef struct
{
int a;
int b;
char c;
}MSG;


int main(int * argc, char * argv[])
{
FILE * fp = fopen("./test.txt", "w+");
if(fp == NULL)
{
printf("error\n");
return -1;
}

MSG msg[4] = {1, 2, 'a', 3, 4, 'b', 5, 6, 'c', 7, 8, 'd'};
fwrite(msg, sizeof(MSG), 4, fp);//先将结构体写入文件


rewind(fp); //将文件指针重新定位到文件开头
MSG rcv[4];
fread(rcv, sizeof(MSG), 4, fp);

for(int i = 0; i < 4; i++){
printf("%d %d %c\n", rcv[i].a, rcv[i].b, rcv[i].c);
}

return 0;
}

fscanf

从文件中读取格式化数据

1
2
3
4
int fscanf(
FILE *stream,
const char *format,
...);

fscanf函数从stream标识的文件中读取格式化数据,格式由format指定,参数由...指定。

用例:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int * argc, char * argv[])
{
FILE * fp = fopen("./test.txt", "w+");
//这里省略对fp的异常处理的代码段
fscanf(fp, "%d %d %d", &a, &b, &c);
printf("a = %d, b = %d, c = %d\n", a, b, c);
return 0;
}

文件:

1
1 2 3

输出结果:

1
a = 1, b = 2, c = 3

返回值:

  • 成功:返回读取的参数个数
  • 失败:返回EOF

参数:

  • stream:要读取的文件指针
  • format:要读取的格式化字符串
  • …:要读取的参数

fprintf

向文件中写入格式化数据

1
2
3
4
int fprintf(
FILE *stream,
const char *format,
...);

fprintf函数向stream标识的文件中写入格式化数据,格式由format指定,参数由...指定。

返回值:

  • 成功:返回写入的字符个数
  • 失败:返回EOF

参数:

  • stream:要写入的文件指针
  • format:要写入的格式化字符串
  • …:要写入的参数

写入数据的方式:

  • 从文件指针的位置开始写入
  • 每次写入一个字符
  • 写入的数据会自动转换为ASCII码


文件的随机读写

在之前的文件指针操作中,介绍了对文件指针的读取位置进行操作和查询的函数,结合文件读写函数就可以实现对文件内容的随机读写。
而除了文件指针操作中介绍的ftell fseek rewind 函数是对文件指针的位置偏移量进行操作

这里我们再介绍两个函数fgetpos fsetpos,这两个函数是直接操作文件指针的位置
使用fgetpos fsetpos可以将文件中的特殊读写位置直接写入到fpos_t类型的变量中,在读写后快速将指针返回到变量储存的位置中,而不需要间接保存偏移量。这样可以确保在文件中进行非顺序操作时不会丢失数据。

fgetpos

获取文件指针当前位置

1
2
3
int fgetpos(
FILE *stream,
fpos_t *pos);

fgetpos函数获取stream标识的文件指针当前位置,并保存在pos中。

返回值:

  • 成功:返回0
  • 失败:返回非0

参数:

  • stream:要获取的文件指针
  • pos:保存文件指针当前位置的变量

fsetpos

将文件指针移动到指定位置

1
2
3
int fsetpos(
FILE *stream,
const fpos_t *pos);

fsetpos函数将stream标识的文件指针移动到pos指定的位置。

返回值:

  • 成功:返回0
  • 失败:返回非0

参数:

  • stream:要移动的文件指针
  • pos:要移动到的位置

1.7k 词

阅读提示:本文使用了html相关控制语句设置单元格背景颜色,建议使用白色背景进行阅读(白天模式)

位段

C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位段”(bit field)。
利用位段能够用较少的位数存储数据。。unsigned int a:1;表示a占1位Bit表示数字。
位段成员除了可以指定为unsigned int整形还可以指定为字符char

1
2
3
4
5
typedef struct{
unsigned int a:1;
unsigned int b:1;
unsigned int c:2;
}Bit;

在底层编程中,使用位段可以很方便地将一个存储单元划分成若干长度不等的连续内存。但其内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。不同的IDE在处理位段这种数据结构的时候确实会有很大的不同。比如vs和DEV-c++。[1]

存储单元

存储单元就是一个C语言的基本变量,如intchar等;
位段必须存放在一个存储单元中,不能跨两个单元,一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。’以下是常见的存储单元大小:

类型 单元大小(Byte) 最大长度(bit)
char 1 8
short 2 16
int 4 32
long 4 32
float 4 32
double 8 64

测试代码: [点击运行]

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
struct temp1{
char a:7;
char b:7;
char c:2;
}temp1;

int main(){
printf("%d\n",sizeof(temp1));
return 0;
}

输出结果:

output
1
3

分析:

char a:7表示a占7位,char b:7表示b占7位,char c:2表示c占2位,一共是16位,但是一个存储单元是8位,且成员不能跨单位存储,所以需要三个存储单元,因此sizeof(temp)的结果是3。

使用表格体现这个位段的内存划分:
存储单元1 存储单元2 存储单元3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
a N b N c N

无名位段

如果位段的定义没有给出标识符名字,那么这是无名位段,无法被初始化。
无名位段用于填充(padding)内存布局。只有无名位段的比特数可以为0
这种占0比特的无名位段,用于强制让下一个位段在内存分配边界对齐。

测试代码: [点击运行]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
struct temp1{
char a:1;
char b:2;
}temp1;
struct temp2{
char a:1;
char : 0;
char b:2;
}temp2;

int main(){
printf("temp1:%d\n",sizeof(temp1));
printf("temp2:%d\n",sizeof(temp2));
return 0;
}

输出结果:

output
1
2
temp1:1
temp2:2

内存划分:

Temp1:
存储单元1
0 1 2 3 4 5 6 7
a b N
Temp2:
存储单元1 存储单元2
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
a N b N

实现

通常在大端序系统(如PowerPC),安排位域从最重要字节(most-significant byte)到最不重要字节(least-significant byte),在一个字节内部从最重要位(most-significant bit)到最不重要位(least-significant bit);

而在小端序系统(如x86),安排位域从最不重要位(least-significant byte)到最重要字节(most-significant byte),在一个字节内部从最不重要位(least-significant bit)到最重要位(most-significant bit)。

共同遵从的原则是内存字节地址从低到高,内存内部的比特编号从低到高。[1]